--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 553ba92b4c59cb9ab403369a123ccf0411039cc8
Parents : 6db7ea7
Author : Mark Qvist <mark@unsigned.io>
Date : 2021-05-13T16:39:31+02:00
Implemented basic conversation list and chat functionality in Text UI
Changes
3 files changed, 369 insertions(+), 97 deletions(-)
Diff
diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py
index 246d9bd..1d13d1e 100644
--- a/nomadnet/ui/TextUI.py
+++ b/nomadnet/ui/TextUI.py
@@ -21,11 +21,19 @@ THEMES = {
('menubar', 'black', 'light gray', 'standout', '#111', '#bbb'),
('shortcutbar', 'black', 'light gray', 'standout', '#111', '#bbb'),
('body_text', 'white', 'default', 'default', '#0a0', 'default'),
+ ('error_text', 'dark red', 'default', 'default', 'dark red', 'default'),
('buttons', 'light green,bold', 'default', 'default', '#00a533', 'default'),
('msg_editor', 'black', 'light cyan', 'standout', '#111', '#0bb'),
- ("msg_header_ok", 'black', 'light green', 'standout', 'black', '#6b2',),
- ("msg_header_caution", 'black', 'yellow', 'standout', 'black', '#fd3',),
- ("list_focus", "black", "light cyan", "standout", "#111", "#0bb"),
+ ("msg_header_ok", 'black', 'light green', 'standout', '#111', '#6b2'),
+ ("msg_header_caution", 'black', 'yellow', 'standout', '#111', '#fd3'),
+ ("msg_header_sent", 'black', 'light gray', 'standout', '#111', '#ddd'),
+ ("msg_header_delivered", 'black', 'light blue', 'standout', '#111', '#28b'),
+ ("msg_warning_untrusted", 'black', 'dark red', 'standout', 'black', 'dark red'),
+ ("list_focus", "black", "light gray", "standout", "#111", "#bbb"),
+ ("list_off_focus", "black", "dark gray", "standout", "#111", "dark gray"),
+ ("list_trusted", "light green", "default", "default", "#6b2", "default"),
+ ("list_unknown", "dark gray", "default", "default", "light gray", "default"),
+ ("list_untrusted", "dark red", "default", "default", "dark red", "default"),
]
}
diff --git a/nomadnet/ui/textui/Conversations.py b/nomadnet/ui/textui/Conversations.py
index 993b398..a22c381 100644
--- a/nomadnet/ui/textui/Conversations.py
+++ b/nomadnet/ui/textui/Conversations.py
@@ -1,151 +1,412 @@
import RNS
import time
import nomadnet
+import LXMF
import urwid
-class ListEntry(urwid.Text):
- _selectable = True
-
- signals = ["click"]
-
- def keypress(self, size, key):
- """
- Send 'click' signal on 'activate' command.
- """
- if self._command_map[key] != urwid.ACTIVATE:
- return key
-
- self._emit('click')
-
- def mouse_event(self, size, event, button, x, y, focus):
- """
- Send 'click' signal on button 1 press.
- """
- if button != 1 or not urwid.util.is_mouse_press(event):
- return False
-
- self._emit('click')
- return True
-
+from datetime import datetime
+from nomadnet.Directory import DirectoryEntry
class ConversationListDisplayShortcuts():
def __init__(self, app):
self.app = app
- self.widget = urwid.AttrMap(urwid.Text("Conversation List Display Shortcuts"), "shortcutbar")
+ self.widget = urwid.AttrMap(urwid.Text("[Enter] Open [C-a] Add to directory [C-x] Delete [C-n] New"), "shortcutbar")
class ConversationDisplayShortcuts():
def __init__(self, app):
self.app = app
- self.widget = urwid.AttrMap(urwid.Text("[C-s] Send"), "shortcutbar")
+ self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-k] Clear [C-t] Add Title [C-w] Close Conversation"), "shortcutbar")
+
+class ConversationsArea(urwid.LineBox):
+ def keypress(self, size, key):
+ if key == "ctrl a":
+ self.delegate.add_selected_to_directory()
+ elif key == "ctrl x":
+ self.delegate.delete_selected_conversation()
+ elif key == "ctrl n":
+ self.delegate.new_conversation()
+ else:
+ return super(ConversationsArea, self).keypress(size, key)
class ConversationsDisplay():
list_width = 0.33
cached_conversation_widgets = {}
def __init__(self, app):
- from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox
-
self.app = app
-
- conversation_list_widgets = []
- for conversation in app.conversations():
- conversation_list_widgets.append(self.conversation_list_widget(conversation))
+ self.dialog_open = False
def disp_list_shortcuts(sender, arg1, arg2):
self.shortcuts_display = self.list_shortcuts
self.app.ui.main_display.update_active_shortcuts()
- RNS.log("Modified")
- walker = urwid.SimpleFocusListWalker(conversation_list_widgets)
- ilb = IndicativeListBox(conversation_list_widgets)
- listbox = urwid.LineBox(urwid.Filler(ilb, height=("relative", 100)))
+ self.update_listbox()
- columns_widget = urwid.Columns([("weight", ConversationsDisplay.list_width, listbox), ("weight", 1-ConversationsDisplay.list_width, self.make_conversation_widget(None))], dividechars=0, focus_column=0, box_columns=[0])
+ self.columns_widget = urwid.Columns([("weight", ConversationsDisplay.list_width, self.listbox), ("weight", 1-ConversationsDisplay.list_width, self.make_conversation_widget(None))], dividechars=0, focus_column=0, box_columns=[0])
self.list_shortcuts = ConversationListDisplayShortcuts(self.app)
self.editor_shortcuts = ConversationDisplayShortcuts(self.app)
self.shortcuts_display = self.list_shortcuts
- self.widget = columns_widget
+ self.widget = self.columns_widget
+ nomadnet.Conversation.created_callback = self.update_conversation_list
+
+ def focus_change_event(self):
+ # This hack corrects buggy styling behaviour in IndicativeListBox
+ if not self.dialog_open:
+ ilb_position = self.ilb.get_selected_position()
+ self.update_conversation_list()
+ self.ilb.select_item(ilb_position)
+
+ def update_listbox(self):
+
+ from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox
+
+ conversation_list_widgets = []
+ for conversation in self.app.conversations():
+ conversation_list_widgets.append(self.conversation_list_widget(conversation))
+
+ walker = urwid.SimpleFocusListWalker(conversation_list_widgets)
+ self.list_widgets = conversation_list_widgets
+ self.ilb = IndicativeListBox(
+ self.list_widgets,
+ on_selection_change=self.conversation_list_selection,
+ initialization_is_selection_change=False,
+ highlight_offFocus="list_off_focus"
+ )
+
+ self.listbox = ConversationsArea(urwid.Filler(self.ilb, height=("relative", 100)))
+ self.listbox.delegate = self
+
+ def delete_selected_conversation(self):
+ self.dialog_open = True
+ source_hash = self.ilb.get_selected_item().source_hash
+
+ def dismiss_dialog(sender):
+ self.update_conversation_list()
+ self.dialog_open = False
+
+ def confirmed(sender):
+ self.dialog_open = False
+ self.delete_conversation(source_hash)
+ nomadnet.Conversation.delete_conversation(source_hash, self.app)
+ self.update_conversation_list()
+
+ dialog = urwid.LineBox(
+ urwid.Pile([
+ urwid.Text("Delete conversation with\n"+self.app.directory.simplest_display_str(bytes.fromhex(source_hash))+"\n", align="center"),
+ urwid.Columns([("weight", 0.45, urwid.Button("Yes", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("No", on_press=dismiss_dialog))])
+ ]), title="?"
+ )
+ bottom = self.listbox
+
+ overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
+
+ options = self.columns_widget.options("weight", ConversationsDisplay.list_width)
+ self.columns_widget.contents[0] = (overlay, options)
+
+ def add_selected_to_directory(self):
+ self.dialog_open = True
+ source_hash = self.ilb.get_selected_item().source_hash
+ display_name = self.ilb.get_selected_item().display_name
+
+ e_id = urwid.Edit(caption="ID : ",edit_text=source_hash)
+ e_name = urwid.Edit(caption="Name : ",edit_text=display_name)
+
+ trust_button_group = []
+ r_untrusted = urwid.RadioButton(trust_button_group, "Untrusted")
+ r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=True)
+ r_trusted = urwid.RadioButton(trust_button_group, "Trusted")
+
+ def dismiss_dialog(sender):
+ self.update_conversation_list()
+ self.dialog_open = False
+
+ def confirmed(sender):
+ try:
+ display_name = e_name.get_edit_text()
+ source_hash = bytes.fromhex(e_id.get_edit_text())
+ trust_level = DirectoryEntry.UNTRUSTED
+ if r_unknown.state == True:
+ trust_level = DirectoryEntry.UNKNOWN
+ elif r_trusted.state == True:
+ trust_level = DirectoryEntry.TRUSTED
+
+ entry = DirectoryEntry(source_hash, display_name, trust_level)
+ self.app.directory.remember(entry)
+ self.update_conversation_list()
+ self.dialog_open = False
+ except Exception as e:
+ RNS.log("Could not save directory entry. The contained exception was: "+str(e), RNS.LOG_VERBOSE)
+ if not dialog_pile.error_display:
+ dialog_pile.error_display = True
+ options = dialog_pile.options(height_type="pack")
+ dialog_pile.contents.append((urwid.Text(""), options))
+ dialog_pile.contents.append((urwid.Text(("error_text", "Could not save entry. Check your input."), align="center"), options))
+
+ dialog_pile = urwid.Pile([
+ e_id,
+ e_name,
+ urwid.Text(""),
+ r_untrusted,
+ r_unknown,
+ r_trusted,
+ urwid.Text(""),
+ urwid.Columns([("weight", 0.45, urwid.Button("Add", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog))])
+ ])
+ dialog_pile.error_display = False
+
+ dialog = urwid.LineBox(dialog_pile, title="Add to Directory")
+ bottom = self.listbox
+
+ overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=2, right=2)
+
+ options = self.columns_widget.options("weight", ConversationsDisplay.list_width)
+ self.columns_widget.contents[0] = (overlay, options)
+
+ def new_conversation(self):
+ pass
+
+ def delete_conversation(self, source_hash):
+ if source_hash in ConversationsDisplay.cached_conversation_widgets:
+ conversation = ConversationsDisplay.cached_conversation_widgets[source_hash]
+ self.close_conversation(conversation)
+
+ def conversation_list_selection(self, arg1, arg2):
+ pass
+
+ def update_conversation_list(self):
+ ilb_position = self.ilb.get_selected_position()
+ self.update_listbox()
+ options = self.columns_widget.options("weight", ConversationsDisplay.list_width)
+ self.columns_widget.contents[0] = (self.listbox, options)
+ self.ilb.select_item(ilb_position)
+ nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.draw_screen()
+
+
+
def display_conversation(self, sender=None, source_hash=None):
+ self.currently_displayed_conversation = source_hash
options = self.widget.options("weight", 1-ConversationsDisplay.list_width)
self.widget.contents[1] = (self.make_conversation_widget(source_hash), options)
def make_conversation_widget(self, source_hash):
- time_format = self.app.time_format
- class LXMessageWidget(urwid.WidgetWrap):
- def __init__(self, message):
- title_string = time.strftime(time_format)
- if message.get_title() != "":
- title_string += " | " + message.get_title()
- if message.signature_validated():
- header_style = "msg_header_ok"
- else:
- header_style = "msg_header_caution"
- title_string = "\u26A0 "+message.get_signature_description() + "\n" + title_string
-
- title = urwid.AttrMap(urwid.Text(title_string), header_style)
-
- display_widget = urwid.Pile([
- title,
- urwid.Text(message.get_content()),
- urwid.Text("")
- ])
-
- urwid.WidgetWrap.__init__(self, display_widget)
+ if source_hash in ConversationsDisplay.cached_conversation_widgets:
+ conversation_widget = ConversationsDisplay.cached_conversation_widgets[source_hash]
+ if source_hash != None:
+ conversation_widget.update_message_widgets(replace=True)
+ return conversation_widget
+ else:
+ widget = ConversationWidget(source_hash)
+ widget.delegate = self
+ ConversationsDisplay.cached_conversation_widgets[source_hash] = widget
+ return widget
+
+ def close_conversation(self, conversation):
+ ConversationsDisplay.cached_conversation_widgets.pop(conversation.source_hash)
+ self.display_conversation(sender=None, source_hash=None)
+
+
+ def conversation_list_widget(self, conversation):
+ trust_level = conversation[2]
+ display_name = conversation[1]
+ source_hash = conversation[0]
+
+ if trust_level == DirectoryEntry.UNTRUSTED:
+ symbol = "x"
+ style = "list_untrusted"
+ elif trust_level == DirectoryEntry.UNKNOWN:
+ symbol = "?"
+ style = "list_unknown"
+ elif trust_level == DirectoryEntry.TRUSTED:
+ symbol = "\u2713"
+ style = "list_trusted"
+ elif trust_level == DirectoryEntry.WARNING:
+ symbol = "\u26A0"
+ style = "list_warning"
+ else:
+ symbol = "\u26A0"
+ style = "list_warning"
+
+ display_text = symbol
+ if display_name != None:
+ display_text += " "+display_name
+
+ if trust_level != DirectoryEntry.TRUSTED:
+ display_text += " <"+source_hash+">"
+
+ widget = ListEntry(display_text)
+ urwid.connect_signal(widget, "click", self.display_conversation, conversation[0])
+ display_widget = urwid.AttrMap(widget, style, "list_focus")
+ display_widget.source_hash = source_hash
+ display_widget.display_name = display_name
+
+ return display_widget
+
+
+ def shortcuts(self):
+ focus_path = self.widget.get_focus_path()
+ if focus_path[0] == 0:
+ return self.list_shortcuts
+ elif focus_path[0] == 1:
+ return self.editor_shortcuts
+ else:
+ return self.list_shortcuts
+
+class ListEntry(urwid.Text):
+ _selectable = True
+
+ signals = ["click"]
+
+ def keypress(self, size, key):
+ """
+ Send 'click' signal on 'activate' command.
+ """
+ if self._command_map[key] != urwid.ACTIVATE:
+ return key
+
+ self._emit('click')
+
+ def mouse_event(self, size, event, button, x, y, focus):
+ """
+ Send 'click' signal on button 1 press.
+ """
+ if button != 1 or not urwid.util.is_mouse_press(event):
+ return False
+
+ self._emit('click')
+ return True
+class MessageEdit(urwid.Edit):
+ def keypress(self, size, key):
+ if key == "ctrl d":
+ self.delegate.send_message()
+ elif key == "ctrl k":
+ self.delegate.clear_editor()
+ elif key == "ctrl w":
+ self.delegate.close()
+ else:
+ return super(MessageEdit, self).keypress(size, key)
+
+
+class ConversationWidget(urwid.WidgetWrap):
+ def __init__(self, source_hash):
if source_hash == None:
- return urwid.LineBox(urwid.Filler(urwid.Text("No conversation selected"), "top"))
+ display_widget = urwid.LineBox(urwid.Filler(urwid.Text("No conversation selected"), "top"))
+ urwid.WidgetWrap.__init__(self, display_widget)
else:
if source_hash in ConversationsDisplay.cached_conversation_widgets:
return ConversationsDisplay.cached_conversation_widgets[source_hash]
else:
- conversation = nomadnet.Conversation(source_hash, self.app)
- message_widgets = []
+ self.source_hash = source_hash
+ self.conversation = nomadnet.Conversation(source_hash, nomadnet.NomadNetworkApp.get_shared_instance())
+ self.message_widgets = []
+ self.updating_message_widgets = False
- for message in conversation.messages:
- message_widget = LXMessageWidget(message)
- message_widgets.append(message_widget)
+ self.update_message_widgets()
+ self.conversation.register_changed_callback(self.conversation_changed)
- from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox
- messagelist = IndicativeListBox(message_widgets)
- msg_editor = urwid.Edit(caption="\u270E", edit_text="", multiline=True)
+ msg_editor = MessageEdit(caption="\u270E", edit_text="", multiline=True)
+ msg_editor.delegate = self
- widget = urwid.LineBox(
- urwid.Frame(
- messagelist,
- footer=urwid.AttrMap(msg_editor, "msg_editor")
- )
- )
+ header = None
+ if self.conversation.trust_level == DirectoryEntry.UNTRUSTED:
+ header = urwid.AttrMap(urwid.Padding(urwid.Text("\u26A0 Warning: Conversation with untrusted peer \u26A0", align="center")), "msg_warning_untrusted")
+
+ self.editor = msg_editor
- def disp_editor_shortcuts(sender, arg1, arg2):
- self.shortcuts_display = self.editor_shortcuts
- self.app.ui.main_display.update_active_shortcuts()
+ self.frame = urwid.Frame(
+ self.messagelist,
+ header=header,
+ footer=urwid.AttrMap(msg_editor, "msg_editor")
+ )
- urwid.connect_signal(msg_editor, "change", disp_editor_shortcuts, "modified event")
+ self.display_widget = urwid.LineBox(
+ self.frame
+ )
- ConversationsDisplay.cached_conversation_widgets[source_hash] = widget
- return widget
+ urwid.WidgetWrap.__init__(self, self.display_widget)
+
+ def conversation_changed(self, conversation):
+ self.update_message_widgets(replace = True)
+
+ def update_message_widgets(self, replace = False):
+ while self.updating_message_widgets:
+ time.sleep(0.5)
+
+ self.updating_message_widgets = True
+ self.message_widgets = []
+ added_hashes = []
+ for message in self.conversation.messages:
+ message_hash = message.get_hash()
+ if not message_hash in added_hashes:
+ added_hashes.append(message_hash)
+ message_widget = LXMessageWidget(message)
+ self.message_widgets.append(message_widget)
+
+ self.message_widgets.sort(key=lambda m: m.timestamp, reverse=False)
+ from nomadnet.vendor.additional_urwid_widgets import IndicativeListBox
+ self.messagelist = IndicativeListBox(self.message_widgets, position = len(self.message_widgets)-1)
- def conversation_list_widget(self, conversation):
- #widget = urwid.SelectableIcon(str(conversation), cursor_position=-1)
- widget = ListEntry(str(conversation))
- urwid.connect_signal(widget, "click", self.display_conversation, conversation)
- return urwid.AttrMap(widget, None, "list_focus")
+ if replace:
+ self.frame.contents["body"] = (self.messagelist, None)
+ nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.draw_screen()
+ self.updating_message_widgets = False
- def shortcuts(self):
- focus_path = self.widget.get_focus_path()
- if focus_path[0] == 0:
- return self.list_shortcuts
- elif focus_path[0] == 1:
- return self.editor_shortcuts
+
+ def clear_editor(self):
+ self.editor.set_edit_text("")
+
+ def send_message(self):
+ content = self.editor.get_edit_text()
+ if not content == "":
+ self.conversation.send(content)
+ self.clear_editor()
+
+ def close(self):
+ self.delegate.close_conversation(self)
+
+
+class LXMessageWidget(urwid.WidgetWrap):
+ def __init__(self, message):
+ app = nomadnet.NomadNetworkApp.get_shared_instance()
+ self.timestamp = message.get_timestamp()
+ time_format = app.time_format
+ message_time = datetime.fromtimestamp(self.timestamp)
+ title_string = message_time.strftime(time_format)
+
+ if app.lxmf_destination.hash == message.lxm.source_hash:
+ if message.lxm.state == LXMF.LXMessage.DELIVERED:
+ header_style = "msg_header_delivered"
+ title_string = "\u2713 " + title_string
+ else:
+ header_style = "msg_header_sent"
+ title_string = "\u2192 " + title_string
else:
- return self.list_shortcuts
\ No newline at end of file
+ if message.signature_validated():
+ header_style = "msg_header_ok"
+ title_string = "\u2713 " + title_string
+ else:
+ header_style = "msg_header_caution"
+ title_string = "\u26A0 "+message.get_signature_description() + "\n " + title_string
+
+ if message.get_title() != "":
+ title_string += " | " + message.get_title()
+
+ title = urwid.AttrMap(urwid.Text(title_string), header_style)
+
+ display_widget = urwid.Pile([
+ title,
+ urwid.Text(message.get_content()),
+ urwid.Text("")
+ ])
+
+ urwid.WidgetWrap.__init__(self, display_widget)
\ No newline at end of file
diff --git a/nomadnet/ui/textui/Main.py b/nomadnet/ui/textui/Main.py
index e641877..6e483f8 100644
--- a/nomadnet/ui/textui/Main.py
+++ b/nomadnet/ui/textui/Main.py
@@ -48,12 +48,15 @@ class MainFrame(urwid.Frame):
def focus_changed(self):
current_focus = self.delegate.widget.get_focus_widgets()[-1]
current_focus_path = self.delegate.widget.get_focus_path()
- RNS.log("Focus changed to: "+str(current_focus_path))
-
+
if len(current_focus_path) > 1:
if current_focus_path[0] == "body":
self.delegate.update_active_shortcuts()
+ if self.delegate.sub_displays.active() == self.delegate.sub_displays.conversations_display:
+ # Needed to refresh indicativelistbox styles on mouse focus change
+ self.delegate.sub_displays.conversations_display.focus_change_event()
+
def mouse_event(self, size, event, button, col, row, focus):
current_focus = self.delegate.widget.get_focus_widgets()[-1]
if current_focus != self.current_focus:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────